from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.utils import timezone
from django.db.models import Q
from .models import (
    AnsibleConfig, AnsibleInventory, AnsibleCredential,
    AnsiblePlaybook, AnsibleJob, AnsibleGlobalSetting
)
from .serializers import (
    AnsibleConfigSerializer, AnsibleConfigListSerializer,
    AnsibleInventorySerializer, AnsibleInventoryListSerializer,
    AnsibleCredentialSerializer, AnsibleCredentialListSerializer,
    AnsiblePlaybookSerializer, AnsibleJobSerializer, AnsibleJobListSerializer,
    AnsibleGlobalSettingSerializer
)
from .utils import AnsibleConnectionManager, AnsibleHealthChecker, AnsibleGlobalSettingsManager
import logging
import os

logger = logging.getLogger(__name__)


class AnsibleConfigViewSet(viewsets.ModelViewSet):
    """Ansible配置管理"""
    queryset = AnsibleConfig.objects.all()
    serializer_class = AnsibleConfigSerializer
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if self.action == 'list':
            return AnsibleConfigListSerializer
        return AnsibleConfigSerializer

    def get_queryset(self):
        queryset = AnsibleConfig.objects.all()

        # 搜索过滤
        search = self.request.query_params.get('search', None)
        if search:
            queryset = queryset.filter(
                Q(name__icontains=search) |
                Q(description__icontains=search) |
                Q(host__icontains=search)
            )

        # 状态过滤
        is_active = self.request.query_params.get('is_active', None)
        if is_active is not None:
            queryset = queryset.filter(is_active=is_active.lower() == 'true')

        # 健康状态过滤
        health_status = self.request.query_params.get('health_status', None)
        if health_status:
            queryset = queryset.filter(health_status=health_status)

        return queryset.order_by('-created_at')

    @action(detail=True, methods=['post'])
    def test_connection(self, request, pk=None):
        """测试连接"""
        config = self.get_object()
        try:
            manager = AnsibleConnectionManager(config)
            result = manager.test_connection()

            # 更新最后检查时间
            config.last_check_time = timezone.now()
            if result['status'] == 'success':
                config.health_status = 'healthy'
                config.health_message = result['message']
            else:
                config.health_status = 'unhealthy'
                config.health_message = result['message']
            config.save()

            return Response(result)
        except Exception as e:
            logger.error(f"测试连接失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'测试连接异常：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['post'])
    def health_check(self, request, pk=None):
        """健康检查"""
        config = self.get_object()
        try:
            result = AnsibleHealthChecker.check_config_health(config)

            # 更新健康状态
            config.health_status = result['status']
            config.health_message = result['message']
            config.last_check_time = timezone.now()
            config.save()

            return Response(result)
        except Exception as e:
            logger.error(f"健康检查失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'健康检查异常：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['post'])
    def health_check_all(self, request):
        """批量健康检查"""
        try:
            results = AnsibleHealthChecker.check_all_configs()
            return Response({
                'status': 'success',
                'results': results
            })
        except Exception as e:
            logger.error(f"批量健康检查失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'批量健康检查异常：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['post'])
    def set_default(self, request, pk=None):
        """设置为默认配置"""
        config = self.get_object()
        try:
            # 取消其他默认配置
            AnsibleConfig.objects.filter(is_default=True).update(is_default=False)
            # 设置当前为默认
            config.is_default = True
            config.save()

            return Response({
                'status': 'success',
                'message': f'已将 {config.name} 设置为默认配置'
            })
        except Exception as e:
            logger.error(f"设置默认配置失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'设置失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['get'])
    def get_inventory(self, request, pk=None):
        """获取inventory内容"""
        config = self.get_object()
        try:
            manager = AnsibleConnectionManager(config)
            result = manager.get_inventory_content()
            return Response(result)
        except Exception as e:
            logger.error(f"获取inventory失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'获取inventory异常：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['get'])
    def hosts(self, request, pk=None):
        """获取Ansible配置的主机信息（配置本身的主机）"""
        config = self.get_object()
        try:
            # 返回配置本身的主机信息
            host_info = {
                'id': config.id,
                'hostname': config.host,
                'private_ip': config.host,  # 使用host字段作为IP
                'ansible_host': config.host,
                'ansible_user': config.username,
                'ansible_port': config.port,
                'status': config.health_status,  # 使用健康检查状态
                'auth_type': config.auth_type,
                'description': f'Ansible控制节点 - {config.name}',
                'group': 'ansible_control',
                'last_check_time': config.last_check_time.isoformat() if config.last_check_time else None,
                'health_message': config.health_message
            }

            return Response({
                'status': 'success',
                'data': [host_info]  # 返回单个主机的列表
            })

        except Exception as e:
            logger.error(f"获取Ansible配置主机失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'获取配置主机失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def destroy(self, request, *args, **kwargs):
        """自定义删除方法，允许删除任何配置"""
        instance = self.get_object()

        try:
            # 如果删除的是默认配置，需要处理默认状态
            if instance.is_default:
                # 查找其他配置，将第一个设为默认
                other_configs = AnsibleConfig.objects.exclude(id=instance.id).first()
                if other_configs:
                    other_configs.is_default = True
                    other_configs.save()
                    logger.info(f"已将配置 '{other_configs.name}' 设为新的默认配置")

            # 先删除相关的Inventory、Playbook、Job等关联数据
            from django.db import connection
            with connection.cursor() as cursor:
                # 删除关联的Job记录
                cursor.execute(
                    "DELETE FROM ansible_management_ansiblejob WHERE config_id = %s",
                    [instance.id]
                )

                # 删除关联的Playbook记录
                cursor.execute(
                    "DELETE FROM ansible_management_ansibleplaybook WHERE config_id = %s",
                    [instance.id]
                )

                # 删除关联的Inventory记录
                cursor.execute(
                    "DELETE FROM ansible_management_ansibleinventory WHERE config_id = %s",
                    [instance.id]
                )

                # 最后删除配置本身
                cursor.execute(
                    "DELETE FROM ansible_management_ansibleconfig WHERE id = %s",
                    [instance.id]
                )

            return Response({
                'status': 'success',
                'message': f'配置 "{instance.name}" 及其相关数据删除成功'
            }, status=status.HTTP_204_NO_CONTENT)
        except Exception as e:
            logger.error(f"删除配置失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'删除失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class AnsibleInventoryViewSet(viewsets.ModelViewSet):
    """Ansible Inventory管理"""
    queryset = AnsibleInventory.objects.all()
    serializer_class = AnsibleInventorySerializer
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if self.action == 'list':
            return AnsibleInventoryListSerializer
        return AnsibleInventorySerializer

    def get_queryset(self):
        queryset = AnsibleInventory.objects.all()

        # 搜索过滤
        search = self.request.query_params.get('search', None)
        if search:
            queryset = queryset.filter(
                Q(name__icontains=search) |
                Q(content__icontains=search) |
                Q(config__name__icontains=search)
            )

        # 配置过滤
        config_id = self.request.query_params.get('config_id', None)
        if config_id:
            queryset = queryset.filter(config_id=config_id)

        # 状态过滤
        is_active = self.request.query_params.get('is_active', None)
        if is_active is not None:
            queryset = queryset.filter(is_active=is_active.lower() == 'true')

        return queryset.order_by('-last_sync_time', '-created_at')

    @action(detail=False, methods=['post'])
    def sync_from_cmdb(self, request):
        """从CMDB按环境分离同步主机信息"""
        try:
            from cmdb.models import Host, HostGroup

            # 获取所有主机（不限制状态）
            hosts = Host.objects.all()

            # 获取默认配置
            default_config = AnsibleConfig.objects.filter(is_default=True).first()
            if not default_config:
                return Response({
                    'status': 'error',
                    'message': '未找到默认Ansible配置'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 按环境分组同步
            environments = [
                ('dev', '开发环境'),
                ('test', '测试环境'),
                ('staging', '预发布环境'),
                ('prod', '生产环境')
            ]

            created_inventories = []
            total_hosts = 0
            total_groups = 0

            for env_code, env_display in environments:
                # 获取该环境的主机（通过主机组的环境字段）
                env_hosts = hosts.filter(host_groups__environment=env_code).distinct()

                if not env_hosts.exists():
                    continue

                # 按主机组组织该环境的数据
                inventory_data = {}

                for host in env_hosts:
                    # 获取该环境的主机组
                    host_groups = host.host_groups.filter(environment=env_code)

                    if not host_groups.exists():
                        # 没有组的主机放到ungrouped
                        if 'ungrouped' not in inventory_data:
                            inventory_data['ungrouped'] = {'hosts': []}
                        inventory_data['ungrouped']['hosts'].append({
                            'hostname': host.hostname,
                            'ansible_host': host.public_ip or host.private_ip,
                            'ansible_port': host.port or 22,
                            'ansible_user': host.username or 'root'
                        })
                    else:
                        for group in host_groups:
                            # 使用英文名称，如果没有则使用中文名称
                            group_name = group.ansible_name or group.name
                            if group_name not in inventory_data:
                                inventory_data[group_name] = {'hosts': []}

                            inventory_data[group_name]['hosts'].append({
                                'hostname': host.hostname,
                                'ansible_host': host.public_ip or host.private_ip,
                                'ansible_port': host.port or 22,
                                'ansible_user': host.username or 'root'
                            })

                # 生成INI格式的inventory内容
                inventory_content = self._generate_inventory_ini(inventory_data)

                # 创建或更新该环境的inventory
                inventory_name = f"CMDB同步-{env_display}({default_config.name})"
                inventory, created = AnsibleInventory.objects.get_or_create(
                    config=default_config,
                    name=inventory_name,
                    defaults={
                        'content': inventory_content,
                        'file_path': f'/tmp/cmdb_inventory_{env_code}',
                        'parsed_data': inventory_data,
                        'hosts_count': self._count_hosts(inventory_data),
                        'groups_count': len(inventory_data),
                        'last_sync_time': timezone.now()
                    }
                )

                if not created:
                    inventory.content = inventory_content
                    inventory.parsed_data = inventory_data
                    inventory.hosts_count = self._count_hosts(inventory_data)
                    inventory.groups_count = len(inventory_data)
                    inventory.last_sync_time = timezone.now()
                    inventory.save()

                created_inventories.append({
                    'environment': env_display,
                    'inventory_id': inventory.id,
                    'hosts_count': inventory.hosts_count,
                    'groups_count': inventory.groups_count
                })

                total_hosts += inventory.hosts_count
                total_groups += inventory.groups_count

            if not created_inventories:
                return Response({
                    'status': 'warning',
                    'message': '没有找到可同步的主机'
                })

            return Response({
                'status': 'success',
                'message': f'成功按环境同步{len(created_inventories)}个Inventory，共{total_hosts}台主机，{total_groups}个组',
                'data': {
                    'inventories': created_inventories,
                    'total_hosts': total_hosts,
                    'total_groups': total_groups
                }
            })

        except Exception as e:
            logger.error(f"从CMDB同步主机失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'同步失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def _generate_inventory_ini(self, inventory_data):
        """生成INI格式的inventory内容"""
        lines = []

        for group_name, group_data in inventory_data.items():
            lines.append(f'[{group_name}]')

            for host in group_data.get('hosts', []):
                host_line = host['hostname']
                if host.get('ansible_host'):
                    host_line += f" ansible_host={host['ansible_host']}"
                if host.get('ansible_port') and host['ansible_port'] != 22:
                    host_line += f" ansible_port={host['ansible_port']}"
                if host.get('ansible_user'):
                    host_line += f" ansible_user={host['ansible_user']}"
                lines.append(host_line)

            lines.append('')  # 空行分隔组

        return '\n'.join(lines)

    def _parse_inventory_content(self, content):
        """解析inventory文件内容"""
        inventory_data = {}
        current_group = None

        for line in content.split('\n'):
            line = line.strip()
            if not line or line.startswith('#'):
                continue

            # 检查是否是组标题
            if line.startswith('[') and line.endswith(']'):
                current_group = line[1:-1]
                if current_group not in inventory_data:
                    inventory_data[current_group] = {'hosts': []}
            elif current_group:
                # 解析主机行
                parts = line.split()
                if parts:
                    hostname = parts[0]
                    host_vars = {}

                    # 解析主机变量
                    for part in parts[1:]:
                        if '=' in part:
                            key, value = part.split('=', 1)
                            host_vars[key] = value

                    host_info = {'hostname': hostname}
                    host_info.update(host_vars)
                    inventory_data[current_group]['hosts'].append(host_info)

        return inventory_data

    def _count_hosts(self, inventory_data):
        """统计主机数量"""
        hosts = set()
        for group_data in inventory_data.values():
            for host in group_data.get('hosts', []):
                hosts.add(host.get('hostname', ''))
        return len(hosts)

    def _deduplicate_hosts(self, inventory_data):
        """去重主机信息 - 基于主机名和IP地址"""
        seen_hosts = {}  # 用于跟踪已见过的主机
        deduplicated_data = {}

        for group_name, group_data in inventory_data.items():
            deduplicated_data[group_name] = {'hosts': []}

            for host in group_data.get('hosts', []):
                hostname = host.get('hostname', '')
                ansible_host = host.get('ansible_host', '')

                # 创建主机的唯一标识（主机名+IP地址）
                host_key = f"{hostname}:{ansible_host}"

                if host_key not in seen_hosts:
                    # 第一次见到这个主机，记录它
                    seen_hosts[host_key] = {
                        'host': host,
                        'groups': [group_name]
                    }
                    deduplicated_data[group_name]['hosts'].append(host)
                else:
                    # 已经见过这个主机，只记录它属于的组
                    seen_hosts[host_key]['groups'].append(group_name)

                    # 检查主机信息是否一致，如果不一致则记录警告
                    existing_host = seen_hosts[host_key]['host']
                    if (existing_host.get('ansible_port') != host.get('ansible_port') or
                        existing_host.get('ansible_user') != host.get('ansible_user')):
                        logger.warning(f"主机 {hostname} ({ansible_host}) 在不同组中有不同的配置信息")

        # 清理空组
        deduplicated_data = {k: v for k, v in deduplicated_data.items() if v['hosts']}

        return deduplicated_data, seen_hosts

    def _export_single_environment(self, config, environment, base_path, inventory_id=None):
        """单环境导出"""
        import os

        # 如果提供了inventory_id，直接使用该ID查找
        if inventory_id:
            try:
                inventory = AnsibleInventory.objects.get(
                    id=inventory_id,
                    config=config
                )
                env_display = inventory.name
            except AnsibleInventory.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': f'未找到指定的Inventory (ID: {inventory_id})'
                }, status=status.HTTP_404_NOT_FOUND)
        else:
            # 兼容旧的环境代码方式
            env_display_map = {
                'dev': '开发环境',
                'test': '测试环境',
                'staging': '预发布环境',
                'prod': '生产环境'
            }

            env_display = env_display_map.get(environment, environment)
            inventory_name = f"CMDB同步-{env_display}({config.name})"

            try:
                inventory = AnsibleInventory.objects.get(
                    config=config,
                    name=inventory_name
                )
            except AnsibleInventory.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': f'未找到{env_display}的Inventory'
                }, status=status.HTTP_404_NOT_FOUND)

        # 使用SSH上传文件到远程主机
        from .utils import AnsibleConnectionManager
        connection_manager = AnsibleConnectionManager(config)
        upload_result = connection_manager.upload_file_to_remote(inventory.content, base_path)

        if upload_result['status'] != 'success':
            return Response({
                'status': 'error',
                'message': f'文件上传失败：{upload_result["message"]}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        return Response({
            'status': 'success',
            'message': f'成功导出{env_display}到 {config.host}:{base_path}',
            'data': {
                'file_path': base_path,
                'environment': env_display,
                'hosts_count': inventory.hosts_count,
                'groups_count': inventory.groups_count
            }
        })

    def _export_merged_environments(self, config, base_path):
        """合并导出所有环境"""
        import os

        environments = [
            ('dev', '开发环境'),
            ('test', '测试环境'),
            ('staging', '预发布环境'),
            ('prod', '生产环境')
        ]

        merged_content = []
        total_hosts = 0
        total_groups = 0
        found_inventories = []

        # 查找所有可用的环境Inventory（所有配置下的）
        for env_code, env_display in environments:
            try:
                # 查找该环境的Inventory（所有配置下的，兼容新旧格式）
                from django.db.models import Q
                inventory = AnsibleInventory.objects.filter(
                    Q(name__startswith=f"CMDB同步-{env_display}(") |  # 新格式：CMDB同步-开发环境(配置名)
                    Q(name=f"CMDB同步-{env_display}")  # 旧格式：CMDB同步-开发环境
                ).first()

                if inventory:
                    found_inventories.append((env_code, env_display, inventory))

            except Exception:
                continue

        if not found_inventories:
            return Response({
                'status': 'error',
                'message': '没有找到可合并的环境Inventory，请先从CMDB同步各环境数据'
            }, status=status.HTTP_404_NOT_FOUND)

        # 合并所有找到的环境
        for env_code, env_display, inventory in found_inventories:
            # 为每个环境的组名添加环境前缀
            env_content = []
            for line in inventory.content.split('\n'):
                line = line.strip()
                if line.startswith('[') and line.endswith(']') and line != '[ungrouped]':
                    # 组名添加环境前缀
                    group_name = line[1:-1]
                    env_content.append(f'[{env_code}_{group_name}]')
                elif line.startswith('[ungrouped]'):
                    env_content.append(f'[{env_code}_ungrouped]')
                else:
                    env_content.append(line)

            merged_content.append(f'# {env_display}')
            merged_content.extend(env_content)
            merged_content.append('')  # 空行分隔

            total_hosts += inventory.hosts_count
            total_groups += inventory.groups_count

        # 确保目录存在
        os.makedirs(os.path.dirname(base_path), exist_ok=True)

        # 写入合并文件
        with open(base_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(merged_content))

        return Response({
            'status': 'success',
            'message': f'成功合并{len(found_inventories)}个环境到 {base_path}',
            'data': {
                'file_path': base_path,
                'mode': 'merged',
                'environments': [env_display for _, env_display, _ in found_inventories],
                'total_hosts': total_hosts,
                'total_groups': total_groups
            }
        })

    def _export_merged_configs(self, config_ids):
        """合并导出多个配置的所有环境（带去重）"""
        import os

        # 收集所有inventory数据进行去重
        all_inventory_data = {}
        exported_configs = []

        for config_id in config_ids:
            try:
                config = AnsibleConfig.objects.get(id=config_id)

                # 查找该配置下的所有Inventory
                inventories = AnsibleInventory.objects.filter(config=config)

                if inventories.exists():
                    config_inventory_data = {}

                    for inventory in inventories:
                        # 合并该配置下的所有inventory数据
                        if inventory.parsed_data:
                            for group_name, group_data in inventory.parsed_data.items():
                                # 为组名添加配置前缀以避免组名冲突
                                prefixed_group_name = f"{config.name}_{group_name}"
                                if prefixed_group_name not in config_inventory_data:
                                    config_inventory_data[prefixed_group_name] = {'hosts': []}
                                config_inventory_data[prefixed_group_name]['hosts'].extend(
                                    group_data.get('hosts', [])
                                )

                    # 将该配置的数据合并到总数据中
                    all_inventory_data.update(config_inventory_data)

                    exported_configs.append({
                        'config_name': config.name,
                        'inventory_count': inventories.count(),
                        'file_path': config.inventory_path
                    })

            except AnsibleConfig.DoesNotExist:
                continue

        if not exported_configs:
            return Response({
                'status': 'error',
                'message': '没有找到可合并的配置或Inventory'
            }, status=status.HTTP_404_NOT_FOUND)

        # 对合并后的数据进行去重
        deduplicated_data, seen_hosts = self._deduplicate_hosts(all_inventory_data)

        # 生成去重后的inventory内容
        merged_content = []
        merged_content.append('# 合并导出 - 已去重')
        merged_content.append(f'# 导出时间: {timezone.now().strftime("%Y-%m-%d %H:%M:%S")}')
        merged_content.append(f'# 原始主机数: {sum(self._count_hosts(config_data) for config_data in [inventory.parsed_data for config in exported_configs for inventory in AnsibleInventory.objects.filter(config__name=config["config_name"]) if inventory.parsed_data])}')
        merged_content.append(f'# 去重后主机数: {len(seen_hosts)}')
        merged_content.append('')

        # 生成去重后的inventory内容
        inventory_content = self._generate_inventory_ini(deduplicated_data)
        merged_content.extend(inventory_content.split('\n'))

        # 生成最终内容
        final_content = '\n'.join(merged_content)

        # 将合并内容写入到所有选中配置的远程主机中
        written_files = []
        upload_errors = []

        for config_id in config_ids:
            try:
                config = AnsibleConfig.objects.get(id=config_id)
                output_path = config.inventory_path

                # 使用SSH上传文件到远程主机
                from .utils import AnsibleConnectionManager
                connection_manager = AnsibleConnectionManager(config)
                upload_result = connection_manager.upload_file_to_remote(final_content, output_path)

                if upload_result['status'] == 'success':
                    written_files.append(f"{config.host}:{output_path}")
                else:
                    upload_errors.append(f"{config.host}:{output_path} - {upload_result['message']}")

            except AnsibleConfig.DoesNotExist:
                continue

        # 构建返回消息
        if upload_errors:
            message = f'合并同步完成：成功 {len(written_files)} 个，失败 {len(upload_errors)} 个'
            status_code = 'warning'
        else:
            message = f'成功合并{len(exported_configs)}个配置到 {len(written_files)} 个远程主机（已去重）'
            status_code = 'success'

        return Response({
            'status': status_code,
            'message': message,
            'data': {
                'written_files': written_files,
                'upload_errors': upload_errors,
                'mode': 'merged_configs',
                'configs': exported_configs,
                'total_hosts': len(seen_hosts),
                'total_groups': len(deduplicated_data),
                'deduplication_info': {
                    'original_hosts': sum(config.get('hosts_count', 0) for config in exported_configs),
                    'deduplicated_hosts': len(seen_hosts),
                    'duplicate_hosts': [host_key for host_key, info in seen_hosts.items() if len(info['groups']) > 1]
                }
            }
        })

    def _export_separate_environments(self, config, base_path):
        """分文件导出各环境"""
        import os
        import paramiko

        environments = [
            ('dev', '开发环境'),
            ('test', '测试环境'),
            ('staging', '预发布环境'),
            ('prod', '生产环境')
        ]

        exported_files = []
        total_hosts = 0
        total_groups = 0

        base_dir = os.path.dirname(base_path)
        base_name = os.path.basename(base_path)
        name_part, ext_part = os.path.splitext(base_name)

        # 建立SSH连接
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            # 连接到远程主机
            ssh.connect(
                hostname=config.host,
                port=config.port,
                username=config.username,
                password=config.password,
                timeout=10
            )

            sftp = ssh.open_sftp()

            # 查找所有可用的环境Inventory（所有配置下的）
            for env_code, env_display in environments:
                try:
                    # 查找该环境的Inventory（所有配置下的，兼容新旧格式）
                    from django.db.models import Q
                    inventory = AnsibleInventory.objects.filter(
                        Q(name__startswith=f"CMDB同步-{env_display}(") |  # 新格式：CMDB同步-开发环境(配置名)
                        Q(name=f"CMDB同步-{env_display}")  # 旧格式：CMDB同步-开发环境
                    ).first()

                    if inventory:
                        # 生成环境专用文件路径
                        env_file_path = os.path.join(base_dir, f"{name_part}_{env_code}{ext_part}").replace('\\', '/')

                        # 确保远程目录存在
                        remote_dir = os.path.dirname(env_file_path)
                        ssh.exec_command(f'mkdir -p {remote_dir}')

                        # 写入远程文件
                        with sftp.open(env_file_path, 'w') as f:
                            f.write(inventory.content)

                        exported_files.append({
                            'environment': env_display,
                            'file_path': env_file_path,
                            'hosts_count': inventory.hosts_count,
                            'groups_count': inventory.groups_count
                        })

                        total_hosts += inventory.hosts_count
                        total_groups += inventory.groups_count

                except Exception:
                    continue

            sftp.close()

        except Exception as e:
            return Response({
                'status': 'error',
                'message': f'SSH连接失败: {str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        finally:
            ssh.close()

        if not exported_files:
            return Response({
                'status': 'error',
                'message': '没有找到可分文件导出的环境Inventory，请先从CMDB同步各环境数据'
            }, status=status.HTTP_404_NOT_FOUND)

        return Response({
            'status': 'success',
            'message': f'成功分文件导出{len(exported_files)}个环境',
            'data': {
                'mode': 'separate',
                'files': exported_files,
                'total_hosts': total_hosts,
                'total_groups': total_groups
            }
        })

    @action(detail=True, methods=['post'])
    def parse_inventory(self, request, pk=None):
        """解析inventory内容"""
        inventory = self.get_object()
        try:
            # 这里可以添加inventory解析逻辑
            # 暂时返回已存储的解析数据
            return Response({
                'status': 'success',
                'data': inventory.parsed_data
            })
        except Exception as e:
            logger.error(f"解析inventory失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'解析失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['post'])
    def export_to_file(self, request, pk=None):
        """导出Inventory到配置文件路径"""
        inventory = self.get_object()
        try:
            import os

            # 获取配置的inventory路径
            inventory_path = inventory.config.inventory_path

            # 确保目录存在
            os.makedirs(os.path.dirname(inventory_path), exist_ok=True)

            # 写入文件
            with open(inventory_path, 'w', encoding='utf-8') as f:
                f.write(inventory.content)

            # 更新文件路径
            inventory.file_path = inventory_path
            inventory.save()

            return Response({
                'status': 'success',
                'message': f'成功导出到 {inventory_path}',
                'data': {
                    'file_path': inventory_path
                }
            })
        except Exception as e:
            logger.error(f"导出inventory到文件失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'导出失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['get'])
    def get_available_environments(self, request):
        """获取指定配置下的可用环境"""
        try:
            config_id = request.query_params.get('config_id')
            if not config_id:
                return Response({
                    'status': 'error',
                    'message': '请提供config_id参数'
                }, status=status.HTTP_400_BAD_REQUEST)

            try:
                config = AnsibleConfig.objects.get(id=config_id)
            except AnsibleConfig.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': '配置不存在'
                }, status=status.HTTP_404_NOT_FOUND)

            # 查找该配置下的所有Inventory
            inventories = AnsibleInventory.objects.filter(config=config)

            # 从名称中提取环境信息
            environments = []
            env_map = {
                '开发环境': 'dev',
                '测试环境': 'test',
                '预发布环境': 'staging',
                '生产环境': 'prod'
            }

            for inventory in inventories:
                for env_display, env_code in env_map.items():
                    if env_display in inventory.name:
                        environments.append({
                            'code': env_code,
                            'name': env_display,
                            'inventory_id': inventory.id,
                            'inventory_name': inventory.name
                        })
                        break
                else:
                    # 手动创建的Inventory，使用实际名称
                    environments.append({
                        'code': 'manual',
                        'name': f'手动创建 - {inventory.name}',
                        'inventory_id': inventory.id,
                        'inventory_name': inventory.name
                    })

            return Response({
                'status': 'success',
                'data': {
                    'config_name': config.name,
                    'environments': environments
                }
            })

        except Exception as e:
            logger.error(f"获取可用环境失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'获取失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['post'])
    def smart_export(self, request):
        """智能导出 - 支持多种导出模式"""
        try:
            import os

            export_mode = request.data.get('export_mode', 'single')  # single, merged, separate
            config_ids = request.data.get('config_ids', [])
            environment = request.data.get('environment', 'prod')  # 单环境导出时指定环境
            inventory_id = request.data.get('inventory_id')  # 指定的inventory ID

            if not config_ids:
                return Response({
                    'status': 'error',
                    'message': '请选择Ansible配置'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 确保config_ids是列表
            if not isinstance(config_ids, list):
                config_ids = [config_ids]

            if export_mode == 'single':
                # 单环境导出：只能选择一个配置
                if len(config_ids) != 1:
                    return Response({
                        'status': 'error',
                        'message': '单环境导出只能选择一个配置'
                    }, status=status.HTTP_400_BAD_REQUEST)

                try:
                    config = AnsibleConfig.objects.get(id=config_ids[0])
                except AnsibleConfig.DoesNotExist:
                    return Response({
                        'status': 'error',
                        'message': '配置不存在'
                    }, status=status.HTTP_404_NOT_FOUND)

                return self._export_single_environment(config, environment, config.inventory_path, inventory_id)

            elif export_mode == 'merged':
                # 合并导出：可以选择多个配置
                return self._export_merged_configs(config_ids)

            elif export_mode == 'separate':
                # 分文件导出：只能选择一个配置
                if len(config_ids) != 1:
                    return Response({
                        'status': 'error',
                        'message': '分文件导出只能选择一个配置'
                    }, status=status.HTTP_400_BAD_REQUEST)

                try:
                    config = AnsibleConfig.objects.get(id=config_ids[0])
                except AnsibleConfig.DoesNotExist:
                    return Response({
                        'status': 'error',
                        'message': '配置不存在'
                    }, status=status.HTTP_404_NOT_FOUND)

                return self._export_separate_environments(config, config.inventory_path)
            else:
                return Response({
                    'status': 'error',
                    'message': '不支持的导出模式'
                }, status=status.HTTP_400_BAD_REQUEST)

        except Exception as e:
            logger.error(f"智能导出失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'导出失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['post'])
    def import_from_file(self, request):
        """从配置文件路径导入Inventory"""
        try:
            config_id = request.data.get('config_id')
            if not config_id:
                return Response({
                    'status': 'error',
                    'message': '缺少config_id参数'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 获取配置
            try:
                config = AnsibleConfig.objects.get(id=config_id)
            except AnsibleConfig.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': '配置不存在'
                }, status=status.HTTP_404_NOT_FOUND)

            inventory_path = config.inventory_path

            # 检查文件是否存在
            import os
            if not os.path.exists(inventory_path):
                return Response({
                    'status': 'error',
                    'message': f'文件不存在: {inventory_path}'
                }, status=status.HTTP_404_NOT_FOUND)

            # 读取文件内容
            with open(inventory_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # 解析inventory内容
            inventory_data = self._parse_inventory_content(content)

            # 创建或更新inventory
            inventory_name = f"从文件导入-{config.name}"
            inventory, created = AnsibleInventory.objects.get_or_create(
                config=config,
                name=inventory_name,
                defaults={
                    'content': content,
                    'file_path': inventory_path,
                    'parsed_data': inventory_data,
                    'hosts_count': self._count_hosts(inventory_data),
                    'groups_count': len(inventory_data),
                    'last_sync_time': timezone.now()
                }
            )

            if not created:
                inventory.content = content
                inventory.parsed_data = inventory_data
                inventory.hosts_count = self._count_hosts(inventory_data)
                inventory.groups_count = len(inventory_data)
                inventory.last_sync_time = timezone.now()
                inventory.save()

            return Response({
                'status': 'success',
                'message': f'成功从 {inventory_path} 导入',
                'data': {
                    'inventory_id': inventory.id,
                    'hosts_count': inventory.hosts_count,
                    'groups_count': inventory.groups_count
                }
            })
        except Exception as e:
            logger.error(f"从文件导入inventory失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'导入失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['post'])
    def batch_import(self, request):
        """批量导入Inventory"""
        try:
            import_type = request.data.get('import_type', 'excel')  # excel, inventory, config

            if import_type == 'excel':
                return self._batch_import_excel(request)
            elif import_type in ['inventory', 'file']:  # 支持inventory和file两种参数名
                return self._batch_import_file(request)
            elif import_type == 'config':
                return self._batch_import_from_config(request)
            else:
                return Response({
                    'status': 'error',
                    'message': '不支持的导入类型'
                }, status=status.HTTP_400_BAD_REQUEST)
        except Exception as e:
            logger.error(f"批量导入失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'批量导入失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['get'])
    def batch_export(self, request):
        """批量导出Inventory"""
        try:
            export_type = request.query_params.get('export_type', 'excel')  # excel, file

            if export_type == 'excel':
                return self._batch_export_excel(request)
            elif export_type == 'file':
                return self._batch_export_file(request)
            else:
                return Response({
                    'status': 'error',
                    'message': '不支持的导出类型'
                }, status=status.HTTP_400_BAD_REQUEST)
        except Exception as e:
            logger.error(f"批量导出失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'批量导出失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['get'])
    def download_template(self, request):
        """下载Excel导入模板"""
        try:
            import pandas as pd
            from django.http import HttpResponse
            import io

            # 创建模板数据
            template_data = {
                '主机组名': ['web服务器组', 'web服务器组', '数据库组'],
                '主机名': ['web01', 'web02', 'db01'],
                'IP地址': ['192.168.1.10', '192.168.1.11', '192.168.1.20'],
                '端口': [22, 22, 22],
                '用户名': ['root', 'root', 'root'],
                '备注': ['Web服务器1', 'Web服务器2', '数据库服务器']
            }

            df = pd.DataFrame(template_data)

            # 创建Excel文件
            output = io.BytesIO()
            with pd.ExcelWriter(output, engine='openpyxl') as writer:
                df.to_excel(writer, sheet_name='主机清单', index=False)

            output.seek(0)

            response = HttpResponse(
                output.getvalue(),
                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
            )
            response['Content-Disposition'] = 'attachment; filename="inventory_template.xlsx"'

            return response
        except Exception as e:
            logger.error(f"下载模板失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'下载模板失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def _batch_import_excel(self, request):
        """从Excel批量导入"""
        import pandas as pd

        file = request.FILES.get('file')
        if not file:
            return Response({
                'status': 'error',
                'message': '请上传Excel文件'
            }, status=status.HTTP_400_BAD_REQUEST)

        config_id = request.data.get('config_id')
        if not config_id:
            return Response({
                'status': 'error',
                'message': '请选择Ansible配置'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            config = AnsibleConfig.objects.get(id=config_id)
        except AnsibleConfig.DoesNotExist:
            return Response({
                'status': 'error',
                'message': '配置不存在'
            }, status=status.HTTP_404_NOT_FOUND)

        try:
            # 读取Excel文件
            df = pd.read_excel(file)

            # 验证必需列
            required_columns = ['主机组名', '主机名', 'IP地址']
            missing_columns = [col for col in required_columns if col not in df.columns]
            if missing_columns:
                return Response({
                    'status': 'error',
                    'message': f'缺少必需列: {", ".join(missing_columns)}'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 按主机组组织数据
            inventory_data = {}
            for _, row in df.iterrows():
                group_name = str(row['主机组名']).strip()
                hostname = str(row['主机名']).strip()
                ip_address = str(row['IP地址']).strip()
                port = int(row.get('端口', 22))
                username = str(row.get('用户名', 'root')).strip()

                if group_name not in inventory_data:
                    inventory_data[group_name] = {'hosts': []}

                inventory_data[group_name]['hosts'].append({
                    'hostname': hostname,
                    'ansible_host': ip_address,
                    'ansible_port': port,
                    'ansible_user': username
                })

            # 生成inventory内容
            inventory_content = self._generate_inventory_ini(inventory_data)

            # 创建inventory
            inventory_name = f"Excel导入({config.name})-{timezone.now().strftime('%Y%m%d_%H%M%S')}"
            inventory = AnsibleInventory.objects.create(
                config=config,
                name=inventory_name,
                content=inventory_content,
                file_path=f'/tmp/excel_import_{timezone.now().timestamp()}',
                parsed_data=inventory_data,
                hosts_count=self._count_hosts(inventory_data),
                groups_count=len(inventory_data),
                last_sync_time=timezone.now()
            )

            return Response({
                'status': 'success',
                'message': f'成功导入{inventory.hosts_count}台主机，{inventory.groups_count}个组',
                'data': {
                    'inventory_id': inventory.id,
                    'hosts_count': inventory.hosts_count,
                    'groups_count': inventory.groups_count
                }
            })
        except Exception as e:
            logger.error(f"Excel导入失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'Excel导入失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def _batch_import_file(self, request):
        """从文件批量导入"""
        file = request.FILES.get('file')
        if not file:
            return Response({
                'status': 'error',
                'message': '请上传Inventory文件'
            }, status=status.HTTP_400_BAD_REQUEST)

        config_id = request.data.get('config_id')
        if not config_id:
            return Response({
                'status': 'error',
                'message': '请选择Ansible配置'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            config = AnsibleConfig.objects.get(id=config_id)
        except AnsibleConfig.DoesNotExist:
            return Response({
                'status': 'error',
                'message': '配置不存在'
            }, status=status.HTTP_404_NOT_FOUND)

        try:
            # 读取文件内容
            content = file.read().decode('utf-8')

            # 解析内容
            inventory_data = self._parse_inventory_content(content)

            # 创建inventory
            inventory_name = f"文件导入({config.name})-{file.name}-{timezone.now().strftime('%Y%m%d_%H%M%S')}"
            inventory = AnsibleInventory.objects.create(
                config=config,
                name=inventory_name,
                content=content,
                file_path=f'/tmp/file_import_{timezone.now().timestamp()}',
                parsed_data=inventory_data,
                hosts_count=self._count_hosts(inventory_data),
                groups_count=len(inventory_data),
                last_sync_time=timezone.now()
            )

            return Response({
                'status': 'success',
                'message': f'成功导入{inventory.hosts_count}台主机，{inventory.groups_count}个组',
                'data': {
                    'inventory_id': inventory.id,
                    'hosts_count': inventory.hosts_count,
                    'groups_count': inventory.groups_count
                }
            })
        except Exception as e:
            logger.error(f"文件导入失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'文件导入失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def _batch_export_excel(self, request):
        """批量导出到Excel"""
        import pandas as pd
        from django.http import HttpResponse
        import io

        # 获取所有inventory
        inventories = self.get_queryset()

        # 准备导出数据
        export_data = []
        for inventory in inventories:
            for group_name, group_data in inventory.parsed_data.items():
                for host in group_data.get('hosts', []):
                    export_data.append({
                        'Inventory名称': inventory.name,
                        '配置名称': inventory.config.name,
                        '主机组名': group_name,
                        '主机名': host.get('hostname', ''),
                        'IP地址': host.get('ansible_host', ''),
                        '端口': host.get('ansible_port', 22),
                        '用户名': host.get('ansible_user', 'root'),
                        '创建时间': inventory.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                        '最后同步': inventory.last_sync_time.strftime('%Y-%m-%d %H:%M:%S') if inventory.last_sync_time else ''
                    })

        if not export_data:
            return Response({
                'status': 'error',
                'message': '没有数据可导出'
            }, status=status.HTTP_400_BAD_REQUEST)

        # 创建DataFrame
        df = pd.DataFrame(export_data)

        # 创建Excel文件
        output = io.BytesIO()
        with pd.ExcelWriter(output, engine='openpyxl') as writer:
            df.to_excel(writer, sheet_name='主机清单', index=False)

        output.seek(0)

        response = HttpResponse(
            output.getvalue(),
            content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )
        response['Content-Disposition'] = f'attachment; filename="inventory_export_{timezone.now().strftime("%Y%m%d_%H%M%S")}.xlsx"'

        return response

    def _batch_export_file(self, request):
        """批量导出到文件"""
        from django.http import HttpResponse
        import zipfile
        import io

        # 获取所有inventory
        inventories = self.get_queryset()

        if not inventories.exists():
            return Response({
                'status': 'error',
                'message': '没有数据可导出'
            }, status=status.HTTP_400_BAD_REQUEST)

        # 创建ZIP文件
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            for inventory in inventories:
                filename = f"{inventory.config.name}_{inventory.name}.ini"
                # 清理文件名中的特殊字符
                filename = "".join(c for c in filename if c.isalnum() or c in (' ', '-', '_', '.')).rstrip()
                zip_file.writestr(filename, inventory.content)

        zip_buffer.seek(0)

        response = HttpResponse(
            zip_buffer.getvalue(),
            content_type='application/zip'
        )
        response['Content-Disposition'] = f'attachment; filename="inventory_files_{timezone.now().strftime("%Y%m%d_%H%M%S")}.zip"'

        return response


class AnsibleCredentialViewSet(viewsets.ModelViewSet):
    """Ansible凭据管理"""
    queryset = AnsibleCredential.objects.all()
    serializer_class = AnsibleCredentialSerializer
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if self.action == 'list':
            return AnsibleCredentialListSerializer
        return AnsibleCredentialSerializer

    def get_queryset(self):
        queryset = AnsibleCredential.objects.all()

        # 搜索过滤
        search = self.request.query_params.get('search', None)
        if search:
            queryset = queryset.filter(
                Q(name__icontains=search) |
                Q(description__icontains=search) |
                Q(username__icontains=search)
            )

        # 凭据类型过滤
        credential_type = self.request.query_params.get('credential_type', None)
        if credential_type:
            queryset = queryset.filter(credential_type=credential_type)

        # 状态过滤
        is_active = self.request.query_params.get('is_active', None)
        if is_active is not None:
            queryset = queryset.filter(is_active=is_active.lower() == 'true')

        return queryset.order_by('-created_at')

    def create(self, request, *args, **kwargs):
        """创建凭据"""
        try:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)

            # 设置创建者
            credential = serializer.save(created_by=request.user)

            logger.info(f"用户 {request.user.username} 创建了凭据: {credential.name}")

            return Response({
                'status': 'success',
                'message': f'凭据 "{credential.name}" 创建成功',
                'data': AnsibleCredentialSerializer(credential).data
            }, status=status.HTTP_201_CREATED)

        except Exception as e:
            logger.error(f"创建凭据失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'创建失败：{str(e)}'
            }, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True, methods=['post'])
    def test_credential(self, request, pk=None):
        """测试凭据连接"""
        try:
            credential = self.get_object()

            # 根据凭据类型进行不同的测试
            if credential.credential_type == 'ssh_key':
                # 测试SSH密钥连接
                test_result = self._test_ssh_key_connection(credential)
            elif credential.credential_type == 'password':
                # 测试密码连接
                test_result = self._test_password_connection(credential)
            elif credential.credential_type == 'aws_key':
                # 测试AWS连接
                test_result = self._test_aws_connection(credential)
            elif credential.credential_type == 'vault':
                # 测试Vault密码
                test_result = self._test_vault_password(credential)
            else:
                return Response({
                    'status': 'error',
                    'message': f'不支持的凭据类型: {credential.credential_type}'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 更新最后使用时间
            if test_result['success']:
                credential.last_used = timezone.now()
                credential.save()

            return Response({
                'status': 'success' if test_result['success'] else 'error',
                'message': test_result['message'],
                'data': test_result
            })

        except Exception as e:
            logger.error(f"测试凭据失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'测试失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['post'])
    def test_all_credentials(self, request):
        """批量测试所有凭据"""
        try:
            credentials = AnsibleCredential.objects.filter(is_active=True)
            results = []

            for credential in credentials:
                try:
                    # 直接调用测试方法而不是通过视图
                    if credential.credential_type == 'ssh_key':
                        test_result = self._test_ssh_key_connection(credential)
                    elif credential.credential_type == 'password':
                        test_result = self._test_password_connection(credential)
                    elif credential.credential_type == 'aws_key':
                        test_result = self._test_aws_connection(credential)
                    elif credential.credential_type == 'vault':
                        test_result = self._test_vault_password(credential)
                    else:
                        test_result = {
                            'success': False,
                            'message': f'不支持的凭据类型: {credential.credential_type}'
                        }

                    # 更新最后使用时间
                    if test_result['success']:
                        credential.last_used = timezone.now()
                        credential.save()

                    results.append({
                        'credential_id': credential.id,
                        'credential_name': credential.name,
                        'success': test_result['success'],
                        'message': test_result['message']
                    })
                except Exception as e:
                    results.append({
                        'credential_id': credential.id,
                        'credential_name': credential.name,
                        'success': False,
                        'message': f'测试失败: {str(e)}'
                    })

            success_count = sum(1 for r in results if r['success'])
            total_count = len(results)

            return Response({
                'status': 'success',
                'message': f'批量测试完成，成功 {success_count}/{total_count}',
                'data': {
                    'total': total_count,
                    'success': success_count,
                    'failed': total_count - success_count,
                    'results': results
                }
            })

        except Exception as e:
            logger.error(f"批量测试凭据失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'批量测试失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['post'])
    def push_public_key(self, request, pk=None):
        """推送公钥到指定主机"""
        try:
            credential = self.get_object()

            # 检查是否为SSH密钥类型
            if credential.credential_type != 'ssh_key':
                return Response({
                    'status': 'error',
                    'message': '只有SSH密钥类型的凭据才能推送公钥'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 检查是否有公钥
            if not credential.public_key:
                return Response({
                    'status': 'error',
                    'message': '该凭据没有公钥内容'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 获取目标主机信息
            host_ids = request.data.get('host_ids', [])
            group_ids = request.data.get('group_ids', [])
            target_username = request.data.get('target_username', 'root')
            auth_method = request.data.get('auth_method', 'password')  # password 或 existing_key
            auth_password = request.data.get('auth_password', '')
            auth_credential_id = request.data.get('auth_credential_id', None)

            if not host_ids and not group_ids:
                return Response({
                    'status': 'error',
                    'message': '请选择要推送的主机或主机组'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 收集所有目标主机
            from cmdb.models import Host, HostGroup
            target_hosts = []

            # 添加直接选择的主机
            if host_ids:
                hosts = Host.objects.filter(id__in=host_ids, status='online')
                target_hosts.extend(hosts)

            # 添加主机组中的主机
            if group_ids:
                groups = HostGroup.objects.filter(id__in=group_ids)
                for group in groups:
                    group_hosts = Host.objects.filter(groups=group, status='online')
                    target_hosts.extend(group_hosts)

            # 去重
            target_hosts = list(set(target_hosts))

            if not target_hosts:
                return Response({
                    'status': 'error',
                    'message': '没有找到可用的在线主机'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 执行公钥推送
            results = self._push_public_key_to_hosts(
                credential, target_hosts, target_username,
                auth_method, auth_password, auth_credential_id
            )

            # 统计结果
            success_count = sum(1 for r in results if r['success'])
            total_count = len(results)

            return Response({
                'status': 'success',
                'message': f'公钥推送完成，成功: {success_count}/{total_count}',
                'data': {
                    'total': total_count,
                    'success': success_count,
                    'failed': total_count - success_count,
                    'results': results
                }
            })

        except Exception as e:
            logger.error(f"推送公钥失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'推送失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['get'])
    def get_push_targets(self, request):
        """获取可用于公钥推送的主机和主机组"""
        try:
            from cmdb.models import Host, HostGroup

            # 获取在线主机
            hosts = Host.objects.filter(status='online').values(
                'id', 'hostname', 'public_ip', 'private_ip', 'port'
            )

            # 获取主机组
            groups = HostGroup.objects.all().values('id', 'name', 'description')

            return Response({
                'status': 'success',
                'data': {
                    'hosts': list(hosts),
                    'groups': list(groups)
                }
            })

        except Exception as e:
            logger.error(f"获取推送目标失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'获取失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def _push_public_key_to_hosts(self, credential, target_hosts, target_username, auth_method, auth_password, auth_credential_id):
        """推送公钥到目标主机"""
        results = []

        for host in target_hosts:
            try:
                result = self._push_public_key_to_single_host(
                    credential, host, target_username, auth_method, auth_password, auth_credential_id
                )
                results.append({
                    'host_id': host.id,
                    'hostname': host.hostname,
                    'ip': host.public_ip or host.private_ip,
                    'success': result['success'],
                    'message': result['message']
                })
            except Exception as e:
                results.append({
                    'host_id': host.id,
                    'hostname': host.hostname,
                    'ip': host.public_ip or host.private_ip,
                    'success': False,
                    'message': f'推送失败: {str(e)}'
                })

        return results

    def _push_public_key_to_single_host(self, credential, host, target_username, auth_method, auth_password, auth_credential_id):
        """推送公钥到单个主机"""
        import paramiko
        import tempfile
        import os

        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            # 确定连接参数
            host_ip = host.public_ip or host.private_ip
            port = host.port or 22

            # 根据认证方式连接
            if auth_method == 'password':
                if not auth_password:
                    return {'success': False, 'message': '密码认证需要提供密码'}
                ssh.connect(host_ip, port=port, username=target_username, password=auth_password, timeout=10)
            elif auth_method == 'existing_key' and auth_credential_id:
                # 使用现有凭据进行认证
                auth_credential = AnsibleCredential.objects.get(id=auth_credential_id)
                if auth_credential.credential_type != 'ssh_key':
                    return {'success': False, 'message': '认证凭据必须是SSH密钥类型'}

                # 创建临时私钥文件
                with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as key_file:
                    key_file.write(auth_credential.private_key)
                    auth_key_path = key_file.name

                os.chmod(auth_key_path, 0o600)

                try:
                    ssh.connect(host_ip, port=port, username=target_username, key_filename=auth_key_path, timeout=10)
                finally:
                    os.unlink(auth_key_path)
            else:
                return {'success': False, 'message': '无效的认证方式'}

            # 准备要推送的公钥
            public_key = credential.public_key.strip()

            # 检查 ~/.ssh 目录是否存在，不存在则创建
            ssh.exec_command(f'mkdir -p /home/{target_username}/.ssh')
            if target_username == 'root':
                ssh.exec_command('mkdir -p /root/.ssh')

            # 检查公钥是否已存在
            authorized_keys_path = f'/home/{target_username}/.ssh/authorized_keys' if target_username != 'root' else '/root/.ssh/authorized_keys'

            stdin, stdout, stderr = ssh.exec_command(f'cat {authorized_keys_path} 2>/dev/null || echo ""')
            existing_keys = stdout.read().decode().strip()

            # 检查公钥是否已经存在
            if public_key in existing_keys:
                return {'success': True, 'message': '公钥已存在，无需重复添加'}

            # 添加公钥到 authorized_keys
            stdin, stdout, stderr = ssh.exec_command(f'echo "{public_key}" >> {authorized_keys_path}')
            stderr_output = stderr.read().decode().strip()

            if stderr_output:
                return {'success': False, 'message': f'添加公钥失败: {stderr_output}'}

            # 设置正确的权限
            ssh.exec_command(f'chmod 700 /home/{target_username}/.ssh' if target_username != 'root' else 'chmod 700 /root/.ssh')
            ssh.exec_command(f'chmod 600 {authorized_keys_path}')
            if target_username != 'root':
                ssh.exec_command(f'chown -R {target_username}:{target_username} /home/{target_username}/.ssh')

            return {'success': True, 'message': '公钥推送成功'}

        except paramiko.AuthenticationException:
            return {'success': False, 'message': '认证失败，请检查用户名和密码/密钥'}
        except paramiko.SSHException as e:
            return {'success': False, 'message': f'SSH连接错误: {str(e)}'}
        except Exception as e:
            return {'success': False, 'message': f'推送失败: {str(e)}'}
        finally:
            ssh.close()

    def _test_ssh_key_connection(self, credential):
        """测试SSH密钥连接"""
        try:
            import paramiko
            import tempfile
            import os

            if not credential.private_key or not credential.username:
                return {
                    'success': False,
                    'message': 'SSH用户名或私钥为空',
                    'credential_type': 'ssh_key'
                }

            # 验证私钥格式
            try:
                # 创建临时私钥文件
                with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as key_file:
                    key_file.write(credential.private_key)
                    key_path = key_file.name

                # 设置正确的权限
                os.chmod(key_path, 0o600)

                # 尝试加载私钥以验证格式
                try:
                    paramiko.RSAKey.from_private_key_file(key_path)
                    key_type = 'RSA'
                except:
                    try:
                        paramiko.Ed25519Key.from_private_key_file(key_path)
                        key_type = 'Ed25519'
                    except:
                        try:
                            paramiko.ECDSAKey.from_private_key_file(key_path)
                            key_type = 'ECDSA'
                        except:
                            return {
                                'success': False,
                                'message': 'SSH私钥格式无效或不支持',
                                'credential_type': 'ssh_key'
                            }

                # 清理临时文件
                os.unlink(key_path)

                return {
                    'success': False,
                    'message': f'SSH私钥格式正确({key_type})，但需要指定测试主机进行实际验证',
                    'credential_type': 'ssh_key',
                    'note': '请在实际使用时通过Ansible任务验证'
                }

            except Exception as e:
                return {
                    'success': False,
                    'message': f'SSH私钥验证失败: {str(e)}',
                    'credential_type': 'ssh_key'
                }

        except Exception as e:
            return {
                'success': False,
                'message': f'SSH密钥测试失败: {str(e)}',
                'credential_type': 'ssh_key'
            }

    def _test_password_connection(self, credential):
        """测试密码连接"""
        try:
            if not credential.username or not credential.password:
                return {
                    'success': False,
                    'message': '用户名或密码为空',
                    'credential_type': 'password'
                }

            # 注意：这里只是验证凭据格式，实际连接测试需要目标主机
            # 在实际环境中，您需要提供一个测试主机来验证凭据
            return {
                'success': False,
                'message': '密码凭据格式正确，但需要指定测试主机进行实际验证',
                'credential_type': 'password',
                'note': '请在实际使用时通过Ansible任务验证'
            }

        except Exception as e:
            return {
                'success': False,
                'message': f'密码测试失败: {str(e)}',
                'credential_type': 'password'
            }

    def _test_aws_connection(self, credential):
        """测试AWS连接"""
        try:
            # 这里应该实际测试AWS连接，暂时返回模拟结果
            if credential.access_key_id and credential.secret_access_key:
                return {
                    'success': True,
                    'message': 'AWS凭据验证成功',
                    'credential_type': 'aws_key'
                }
            else:
                return {
                    'success': False,
                    'message': 'AWS访问密钥不完整',
                    'credential_type': 'aws_key'
                }
        except Exception as e:
            return {
                'success': False,
                'message': f'AWS测试失败: {str(e)}',
                'credential_type': 'aws_key'
            }

    def _test_vault_password(self, credential):
        """测试Vault密码"""
        try:
            # 这里应该实际测试Vault密码，暂时返回模拟结果
            if credential.vault_password:
                return {
                    'success': True,
                    'message': 'Vault密码验证成功',
                    'credential_type': 'vault'
                }
            else:
                return {
                    'success': False,
                    'message': 'Vault密码为空',
                    'credential_type': 'vault'
                }
        except Exception as e:
            return {
                'success': False,
                'message': f'Vault测试失败: {str(e)}',
                'credential_type': 'vault'
            }


class AnsibleGlobalSettingViewSet(viewsets.ModelViewSet):
    """Ansible全局设置管理"""
    queryset = AnsibleGlobalSetting.objects.all()
    serializer_class = AnsibleGlobalSettingSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        queryset = AnsibleGlobalSetting.objects.all()

        # 搜索过滤
        search = self.request.query_params.get('search', None)
        if search:
            queryset = queryset.filter(
                Q(key__icontains=search) |
                Q(description__icontains=search)
            )

        # 类型过滤
        setting_type = self.request.query_params.get('setting_type', None)
        if setting_type:
            queryset = queryset.filter(setting_type=setting_type)

        # 可编辑过滤
        is_editable = self.request.query_params.get('is_editable', None)
        if is_editable is not None:
            queryset = queryset.filter(is_editable=is_editable.lower() == 'true')

        return queryset.order_by('key')

    @action(detail=False, methods=['post'])
    def initialize_defaults(self, request):
        """初始化默认设置"""
        try:
            AnsibleGlobalSettingsManager.initialize_default_settings()
            return Response({
                'status': 'success',
                'message': '默认设置初始化完成'
            })
        except Exception as e:
            logger.error(f"初始化默认设置失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'初始化失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=False, methods=['get'])
    def get_by_key(self, request):
        """根据key获取设置"""
        key = request.query_params.get('key')
        if not key:
            return Response({
                'status': 'error',
                'message': '缺少key参数'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            setting = AnsibleGlobalSetting.objects.get(key=key)
            serializer = self.get_serializer(setting)
            return Response({
                'status': 'success',
                'data': serializer.data
            })
        except AnsibleGlobalSetting.DoesNotExist:
            return Response({
                'status': 'error',
                'message': f'设置 {key} 不存在'
            }, status=status.HTTP_404_NOT_FOUND)

    @action(detail=False, methods=['post'])
    def set_by_key(self, request):
        """根据key设置值"""
        key = request.data.get('key')
        value = request.data.get('value')
        setting_type = request.data.get('setting_type', 'string')
        description = request.data.get('description', '')

        if not key or value is None:
            return Response({
                'status': 'error',
                'message': '缺少key或value参数'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            setting = AnsibleGlobalSettingsManager.set_setting(
                key=key,
                value=value,
                setting_type=setting_type,
                description=description
            )
            serializer = self.get_serializer(setting)
            return Response({
                'status': 'success',
                'message': f'设置 {key} 更新成功',
                'data': serializer.data
            })
        except Exception as e:
            logger.error(f"设置值失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'设置失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


# 统计视图
class AnsibleStatsViewSet(viewsets.ViewSet):
    """Ansible统计信息"""
    permission_classes = [IsAuthenticated]

    @action(detail=False, methods=['get'])
    def dashboard(self, request):
        """获取仪表板统计数据"""
        try:
            # 配置统计
            total_configs = AnsibleConfig.objects.count()
            active_configs = AnsibleConfig.objects.filter(is_active=True).count()
            healthy_configs = AnsibleConfig.objects.filter(health_status='healthy').count()

            # 任务统计
            total_jobs = AnsibleJob.objects.count()
            running_jobs = AnsibleJob.objects.filter(status='running').count()
            success_jobs = AnsibleJob.objects.filter(status='success').count()
            failed_jobs = AnsibleJob.objects.filter(status='failed').count()

            # 计算成功率
            success_rate = 0
            if total_jobs > 0:
                success_rate = round((success_jobs / total_jobs) * 100, 2)

            # 凭据统计
            total_credentials = AnsibleCredential.objects.count()
            active_credentials = AnsibleCredential.objects.filter(is_active=True).count()

            # Playbook统计
            total_playbooks = AnsiblePlaybook.objects.count()
            active_playbooks = AnsiblePlaybook.objects.filter(is_active=True).count()

            return Response({
                'status': 'success',
                'data': {
                    'configs': {
                        'total': total_configs,
                        'active': active_configs,
                        'healthy': healthy_configs,
                        'health_rate': round((healthy_configs / active_configs) * 100, 2) if active_configs > 0 else 0
                    },
                    'jobs': {
                        'total': total_jobs,
                        'running': running_jobs,
                        'success': success_jobs,
                        'failed': failed_jobs,
                        'success_rate': success_rate
                    },
                    'credentials': {
                        'total': total_credentials,
                        'active': active_credentials
                    },
                    'playbooks': {
                        'total': total_playbooks,
                        'active': active_playbooks
                    }
                }
            })
        except Exception as e:
            logger.error(f"获取统计数据失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'获取统计数据失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    def _batch_import_from_config(self, request):
        """从配置文件导入Inventory"""
        config_id = request.data.get('config_id')
        if not config_id:
            return Response({
                'status': 'error',
                'message': '请选择Ansible配置'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            config = AnsibleConfig.objects.get(id=config_id)
        except AnsibleConfig.DoesNotExist:
            return Response({
                'status': 'error',
                'message': '配置不存在'
            }, status=status.HTTP_404_NOT_FOUND)

        try:
            # 读取配置文件中的inventory文件
            inventory_path = config.inventory_file
            if not inventory_path or not os.path.exists(inventory_path):
                return Response({
                    'status': 'error',
                    'message': '配置文件中的inventory文件不存在'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 读取inventory文件内容
            with open(inventory_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # 解析内容
            inventory_data = self._parse_inventory_content(content)

            # 创建inventory
            inventory = AnsibleInventory.objects.create(
                name=f"从配置导入({config.name})-{timezone.now().strftime('%Y%m%d_%H%M%S')}",
                content=content,
                config=config,
                hosts_count=inventory_data['hosts_count'],
                groups_count=inventory_data['groups_count']
            )

            return Response({
                'status': 'success',
                'message': '从配置文件导入成功',
                'data': {
                    'id': inventory.id,
                    'name': inventory.name,
                    'hosts_count': inventory.hosts_count,
                    'groups_count': inventory.groups_count
                }
            })
        except Exception as e:
            logger.error(f"从配置文件导入失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'从配置文件导入失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class AnsibleJobViewSet(viewsets.ModelViewSet):
    """Ansible任务执行管理"""
    queryset = AnsibleJob.objects.all()
    serializer_class = AnsibleJobSerializer
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if self.action == 'list':
            return AnsibleJobListSerializer
        return AnsibleJobSerializer

    def get_queryset(self):
        queryset = AnsibleJob.objects.all()

        # 搜索过滤
        search = self.request.query_params.get('search', None)
        if search:
            queryset = queryset.filter(
                Q(name__icontains=search) |
                Q(module_name__icontains=search) |
                Q(inventory__icontains=search)
            )

        # 状态过滤
        status_filter = self.request.query_params.get('status', None)
        if status_filter:
            queryset = queryset.filter(status=status_filter)

        # 任务类型过滤
        job_type = self.request.query_params.get('job_type', None)
        if job_type:
            queryset = queryset.filter(job_type=job_type)

        return queryset.order_by('-created_at')

    @action(detail=False, methods=['post'])
    def run_adhoc(self, request):
        """执行Ad-hoc命令"""
        try:
            inventory_id = request.data.get('inventory_id')
            command = request.data.get('command', '').strip()
            credential_id = request.data.get('credential_id')  # 新增凭据ID

            if not inventory_id:
                return Response({
                    'status': 'error',
                    'message': '请选择主机清单'
                }, status=status.HTTP_400_BAD_REQUEST)

            if not command:
                return Response({
                    'status': 'error',
                    'message': '请输入要执行的命令'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 获取inventory
            try:
                inventory = AnsibleInventory.objects.get(id=inventory_id)
            except AnsibleInventory.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': '指定的主机清单不存在'
                }, status=status.HTTP_404_NOT_FOUND)

            # 获取凭据（如果指定了）
            credential = None
            if credential_id:
                try:
                    credential = AnsibleCredential.objects.get(id=credential_id, is_active=True)
                except AnsibleCredential.DoesNotExist:
                    return Response({
                        'status': 'error',
                        'message': '指定的凭据不存在或已禁用'
                    }, status=status.HTTP_404_NOT_FOUND)

            # 解析命令
            command_parts = command.split()
            if len(command_parts) < 3 or command_parts[0] != 'ansible':
                return Response({
                    'status': 'error',
                    'message': '命令格式错误，应为: ansible <pattern> -m <module> [options]'
                }, status=status.HTTP_400_BAD_REQUEST)

            # 提取命令参数
            pattern = command_parts[1]
            module_name = 'ping'  # 默认模块
            module_args = ''

            # 解析命令参数
            i = 2
            while i < len(command_parts):
                if command_parts[i] == '-m' and i + 1 < len(command_parts):
                    module_name = command_parts[i + 1]
                    i += 2
                elif command_parts[i] == '-a' and i + 1 < len(command_parts):
                    # 获取-a后面的参数，支持引号和无引号
                    args_start = i + 1

                    # 重新解析原始命令，找到-a后面的内容
                    command_str = ' '.join(command_parts)
                    a_index = command_str.find(' -a ')
                    if a_index != -1:
                        # 找到-a后面的内容
                        after_a = command_str[a_index + 4:].strip()

                        # 处理引号包围的参数
                        if after_a.startswith('"') or after_a.startswith("'"):
                            quote_char = after_a[0]
                            end_quote = after_a.find(quote_char, 1)
                            if end_quote != -1:
                                module_args = after_a[1:end_quote]
                            else:
                                # 没有结束引号，取到字符串末尾
                                module_args = after_a[1:]
                        else:
                            # 没有引号，取第一个单词或到下一个选项
                            next_option = -1
                            for opt in [' -', ' --']:
                                pos = after_a.find(opt)
                                if pos != -1:
                                    if next_option == -1 or pos < next_option:
                                        next_option = pos

                            if next_option != -1:
                                module_args = after_a[:next_option].strip()
                            else:
                                module_args = after_a.strip()

                    # 跳过已处理的参数
                    break
                else:
                    i += 1

            # 创建任务记录
            job = AnsibleJob.objects.create(
                name=f"Ad-hoc: {module_name} on {pattern}",
                job_type='adhoc',
                status='pending',
                config=inventory.config,
                inventory=inventory.name,
                module_name=module_name,
                module_args=module_args,
                limit=pattern,
                credential=credential,  # 关联凭据
                created_by=request.user
            )

            # 异步执行任务
            from django.utils import timezone
            import threading

            def execute_job():
                try:
                    job.status = 'running'
                    job.start_time = timezone.now()
                    job.save()

                    logger.info(f"开始执行任务 {job.id}: {module_name} {module_args} on {pattern}")

                    # 调用真实的Ansible执行逻辑
                    from .utils import AnsibleConnectionManager

                    manager = AnsibleConnectionManager(inventory.config)

                    # 执行真实的Ansible命令
                    logger.info(f"调用AnsibleConnectionManager执行命令")
                    result = manager.execute_adhoc_command(
                        inventory_content=inventory.content,
                        module_name=module_name,
                        module_args=module_args,
                        pattern=pattern,
                        credential=credential  # 传递凭据
                    )
                    logger.info(f"命令执行完成，结果: {result['status']}")

                    # 更新任务状态
                    job.status = result['status']
                    job.stdout = result.get('stdout', '')
                    job.stderr = result.get('stderr', '')
                    job.result = result.get('result', {})

                    job.end_time = timezone.now()
                    job.duration = int((job.end_time - job.start_time).total_seconds())
                    job.save()

                    logger.info(f"任务 {job.id} 执行完成，状态: {job.status}")

                except Exception as e:
                    job.status = 'failed'
                    job.stderr = str(e)
                    job.end_time = timezone.now()
                    if job.start_time:
                        job.duration = int((job.end_time - job.start_time).total_seconds())
                    job.save()

            # 启动后台线程执行
            thread = threading.Thread(target=execute_job)
            thread.daemon = True
            thread.start()

            return Response({
                'status': 'success',
                'message': 'Ad-hoc命令已启动',
                'job_id': job.id,
                'data': {
                    'job_id': job.id,
                    'name': job.name,
                    'status': job.status,
                    'module_name': module_name,
                    'module_args': module_args,
                    'pattern': pattern
                }
            })

        except Exception as e:
            logger.error(f"执行Ad-hoc命令失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'执行失败：{str(e)}'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(detail=True, methods=['get'])
    def get_status(self, request, pk=None):
        """获取任务状态"""
        job = self.get_object()

        # 优先使用保存的格式化输出（new_output）
        stdout_lines = []
        if job.extra_vars and 'new_output' in job.extra_vars:
            # 使用保存的格式化输出
            stdout_lines = job.extra_vars['new_output']
        elif job.stdout:
            # 如果没有格式化输出，则解析stdout（简化版本）
            current_task_name = ''  # 跟踪当前任务名
            current_play_name = ''  # 跟踪当前play名

            lines = job.stdout.split('\n')
            i = 0

            while i < len(lines):
                line = lines[i]
                if line.strip():
                    # 根据内容判断行类型和详情
                    line_type = 'normal'
                    details = None

                    if 'PLAY [' in line and '*' in line:
                        line_type = 'play'
                        # 提取play名称
                        try:
                            start = line.find('[') + 1
                            end = line.find(']', start)
                            if start > 0 and end > start:
                                current_play_name = line[start:end].strip()
                        except:
                            pass
                    elif 'TASK [' in line and '*' in line:
                        line_type = 'task'
                        # 提取任务名称
                        try:
                            start = line.find('[') + 1
                            end = line.find(']', start)
                            if start > 0 and end > start:
                                current_task_name = line[start:end].strip()
                        except:
                            pass
                    elif 'PLAY RECAP' in line and '*' in line:
                        line_type = 'recap'
                    elif ('changed:' in line or 'ok:' in line or 'failed:' in line or 'fatal:' in line) and '[' in line and ']' in line:
                        # 这是主机状态行，如: ok: [devops1], changed: [devops1] => {...}, fatal: [devops1]: FAILED! => {...}
                        if 'changed:' in line:
                            line_type = 'changed'
                        elif 'failed:' in line or 'fatal:' in line:
                            line_type = 'failed'
                        else:
                            line_type = 'ok'

                        # 提取主机名
                        hostname = 'unknown'
                        try:
                            if '[' in line and ']' in line:
                                start = line.find('[')
                                end = line.find(']', start)
                                if start != -1 and end != -1:
                                    hostname = line[start+1:end].strip()
                        except:
                            pass

                        # 初始化基本详情信息
                        details = {
                            'hostname': hostname,
                            'status': line_type,
                            'task_result': line_type.upper(),
                            'task_name': current_task_name,
                            'play_name': current_play_name
                        }

                        # 提取JSON详情（如果有的话）
                        try:
                            json_start = line.find('=>')
                            if json_start != -1:
                                json_part = line[json_start + 2:].strip()
                                if json_part.startswith('{') and json_part.endswith('}'):
                                    import json
                                    json_details = json.loads(json_part)
                                    details.update(json_details)  # 合并JSON详情
                                    # 只保留主机状态部分，不包含JSON
                                    line = line[:json_start].strip()
                            else:
                                # 没有JSON输出的简单状态行，不添加假数据
                                # 只保留基本的主机和任务信息
                                details['note'] = '此任务没有详细输出（如setup、meta等内置模块）'
                        except Exception as e:
                            print(f"JSON解析错误: {e}")
                            # 即使JSON解析失败，也保留基本详情
                            pass
                    elif ':' in line and ('ok=' in line or 'changed=' in line or 'failed=' in line):
                        # PLAY RECAP中的主机统计行
                        line_type = 'host_stats'

                    stdout_lines.append({
                        'content': line,
                        'type': line_type,
                        'timestamp': job.start_time.isoformat() if job.start_time else None,
                        'details': details
                    })

                i += 1

        # 解析执行统计信息
        stats = {}
        if job.stdout and 'PLAY RECAP' in job.stdout:
            # 尝试从stdout中提取统计信息
            recap_section = job.stdout.split('PLAY RECAP')[1] if 'PLAY RECAP' in job.stdout else ''
            if recap_section:
                import re
                # 匹配类似 "ok=2    changed=1    unreachable=0    failed=0" 的模式
                stats_match = re.search(r'ok=(\d+).*?changed=(\d+).*?unreachable=(\d+).*?failed=(\d+)', recap_section)
                if stats_match:
                    stats = {
                        'ok': int(stats_match.group(1)),
                        'changed': int(stats_match.group(2)),
                        'unreachable': int(stats_match.group(3)),
                        'failed': int(stats_match.group(4))
                    }

        # 计算进度
        progress = 0
        if job.status == 'success':
            progress = 100
        elif job.status == 'failed':
            progress = 100
        elif job.status == 'running':
            progress = 50  # 运行中设为50%

        # 直接返回状态数据，不包装在data字段中
        return Response({
            'status': job.status,
            'progress': progress,
            'current_task': '执行完成' if job.status in ['success', 'failed'] else '正在执行...',
            'new_output': stdout_lines,  # AWX风格的输出行
            'stats': stats,
            'job_id': job.id,
            'name': job.name,
            'start_time': job.start_time,
            'end_time': job.end_time,
            'duration': job.duration,
            'result': job.result,
            'stdout': job.stdout,
            'stderr': job.stderr
        })

    @action(detail=True, methods=['post'])
    def cancel(self, request, pk=None):
        """取消任务"""
        job = self.get_object()
        if job.status in ['pending', 'running']:
            job.status = 'cancelled'
            job.end_time = timezone.now()
            if job.start_time:
                job.duration = int((job.end_time - job.start_time).total_seconds())
            job.save()

            return Response({
                'status': 'success',
                'message': '任务已取消'
            })
        else:
            return Response({
                'status': 'error',
                'message': '任务无法取消'
            }, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=False, methods=['post'])
    def run_playbook(self, request):
        """执行Playbook - AWX风格的API"""
        try:
            # 获取参数
            inventory_id = request.data.get('inventory_id')
            credential_id = request.data.get('credential_id')
            playbook_content = request.data.get('playbook_content')
            playbook_name = request.data.get('playbook_name', 'main.yml')
            extra_vars = request.data.get('extra_vars', {})
            limit = request.data.get('limit', '')
            tags = request.data.get('tags', '')

            # 验证必要参数
            if not all([inventory_id, playbook_content]):
                return Response({
                    'status': 'error',
                    'message': '缺少必要参数：inventory_id, playbook_content'
                }, status=400)

            # 获取inventory
            try:
                inventory = AnsibleInventory.objects.get(id=inventory_id)
            except AnsibleInventory.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': '指定的主机清单不存在'
                }, status=404)

            # 获取凭据（如果提供）
            credential = None
            if credential_id:
                try:
                    credential = AnsibleCredential.objects.get(id=credential_id)
                except AnsibleCredential.DoesNotExist:
                    return Response({
                        'status': 'error',
                        'message': '指定的凭据不存在'
                    }, status=404)

            # 创建任务记录
            job = AnsibleJob.objects.create(
                name=f"Playbook执行 - {playbook_name}",
                job_type='playbook',
                status='pending',
                config=inventory.config,
                credential=credential,
                inventory=inventory.content,
                extra_vars=extra_vars,
                tags=tags,
                limit=limit,
                created_by=request.user
            )

            # 异步执行Playbook
            from django.utils import timezone
            import threading

            def execute_playbook_job():
                try:
                    job.status = 'running'
                    job.start_time = timezone.now()
                    job.save()

                    logger.info(f"开始执行Playbook任务 {job.id}")

                    # 调用Ansible执行逻辑
                    from .utils import AnsibleConnectionManager

                    manager = AnsibleConnectionManager(inventory.config)

                    # 执行Playbook
                    result = manager.execute_playbook(
                        inventory_content=job.inventory,
                        playbook_content=playbook_content,
                        credential=credential
                    )

                    # 更新任务状态
                    job.status = result['status']
                    job.stdout = result.get('stdout', '')
                    job.stderr = result.get('stderr', '')
                    job.result = result.get('result', {})

                    # 保存格式化输出（前端需要的new_output）
                    if 'new_output' in result:
                        job.extra_vars = job.extra_vars or {}
                        job.extra_vars['new_output'] = result['new_output']

                    job.end_time = timezone.now()
                    job.duration = int((job.end_time - job.start_time).total_seconds())
                    job.save()

                    logger.info(f"Playbook任务 {job.id} 执行完成，状态: {job.status}")

                except Exception as e:
                    job.status = 'failed'
                    job.stderr = str(e)
                    job.end_time = timezone.now()
                    if job.start_time:
                        job.duration = int((job.end_time - job.start_time).total_seconds())
                    job.save()
                    logger.error(f"Playbook任务 {job.id} 执行失败: {str(e)}")

            # 启动后台线程执行
            thread = threading.Thread(target=execute_playbook_job)
            thread.daemon = True
            thread.start()

            return Response({
                'status': 'success',
                'message': 'Playbook任务已提交，开始执行',
                'job_id': job.id,
                'data': {
                    'job_id': job.id,
                    'name': job.name,
                    'status': job.status,
                    'playbook_name': playbook_name
                }
            })

        except Exception as e:
            logger.error(f"提交Playbook执行任务失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'提交任务失败：{str(e)}'
            }, status=500)

    @action(detail=False, methods=['post'])
    def execute_playbook(self, request):
        """执行Playbook（集成凭据管理）"""
        try:
            host_id = request.data.get('host_id')
            playbook_content = request.data.get('playbook_content')
            inventory_id = request.data.get('inventory_id')
            credential_id = request.data.get('credential_id')
            playbook_path = request.data.get('playbook_path', 'web_editor.yml')

            # 验证必要参数
            if not all([host_id, inventory_id]):
                return Response({
                    'status': 'error',
                    'message': '缺少必要参数：host_id, inventory_id'
                }, status=400)

            # 如果没有playbook_content，尝试从playbook_path读取
            if not playbook_content and playbook_path:
                # 获取Ansible配置信息
                from .models import AnsibleConfig
                try:
                    config = AnsibleConfig.objects.get(id=host_id)
                except AnsibleConfig.DoesNotExist:
                    return Response({
                        'status': 'error',
                        'message': f'Ansible配置不存在: {host_id}'
                    }, status=404)

                # 从远程主机读取文件内容
                import paramiko
                import tempfile
                import os

                ssh = paramiko.SSHClient()
                ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

                try:
                    # 连接到主机
                    if config.auth_type == 'password':
                        ssh.connect(
                            hostname=config.host,
                            port=config.port,
                            username=config.username,
                            password=config.password,
                            timeout=10
                        )
                    elif config.auth_type == 'key':
                        with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as key_file:
                            key_file.write(config.private_key)
                            key_path = key_file.name

                        try:
                            ssh.connect(
                                hostname=config.host,
                                port=config.port,
                                username=config.username,
                                key_filename=key_path,
                                timeout=10
                            )
                        finally:
                            os.unlink(key_path)

                    # 读取文件内容
                    stdin, stdout, stderr = ssh.exec_command(f'cat "{playbook_path}"')
                    playbook_content = stdout.read().decode('utf-8')
                    error = stderr.read().decode('utf-8')

                    if error:
                        return Response({
                            'status': 'error',
                            'message': f'读取Playbook文件失败: {error}'
                        }, status=500)

                finally:
                    ssh.close()

            # 最终验证playbook_content
            if not playbook_content:
                return Response({
                    'status': 'error',
                    'message': '缺少Playbook内容或无法读取Playbook文件'
                }, status=400)

            # 验证凭据
            credential = None
            if credential_id:
                try:
                    credential = AnsibleCredential.objects.get(id=credential_id, is_active=True)
                except AnsibleCredential.DoesNotExist:
                    return Response({
                        'status': 'error',
                        'message': '指定的凭据不存在或已停用'
                    }, status=400)

            # 获取inventory
            try:
                inventory = AnsibleInventory.objects.get(id=inventory_id)
            except AnsibleInventory.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': '指定的Inventory不存在'
                }, status=400)

            # 创建执行任务（为审计预留完整信息）
            job = AnsibleJob.objects.create(
                name=f"Playbook执行 - {playbook_path}",
                job_type='playbook',
                status='pending',
                config=inventory.config,
                credential=credential,
                inventory=inventory.content,
                created_by=request.user,
                extra_vars={
                    'host_id': host_id,
                    'playbook_path': playbook_path,
                    'execution_source': 'web_editor',
                    'client_ip': request.META.get('REMOTE_ADDR'),
                    'user_agent': request.META.get('HTTP_USER_AGENT')
                }
            )

            # 异步执行Playbook
            from django.utils import timezone
            import threading

            def execute_playbook_job():
                try:
                    job.status = 'running'
                    job.start_time = timezone.now()
                    job.save()

                    logger.info(f"开始执行Playbook任务 {job.id}")

                    # 调用Ansible执行逻辑
                    from .utils import AnsibleConnectionManager

                    manager = AnsibleConnectionManager(inventory.config)

                    # 执行Playbook
                    result = manager.execute_playbook(
                        inventory_content=job.inventory,
                        playbook_content=playbook_content,
                        credential=credential
                    )

                    # 更新任务状态
                    job.status = result['status']
                    job.stdout = result.get('stdout', '')
                    job.stderr = result.get('stderr', '')
                    job.result = result.get('result', {})

                    job.end_time = timezone.now()
                    job.duration = int((job.end_time - job.start_time).total_seconds())
                    job.save()

                    logger.info(f"Playbook任务 {job.id} 执行完成，状态: {job.status}")

                except Exception as e:
                    job.status = 'failed'
                    job.stderr = str(e)
                    job.end_time = timezone.now()
                    if job.start_time:
                        job.duration = int((job.end_time - job.start_time).total_seconds())
                    job.save()
                    logger.error(f"Playbook任务 {job.id} 执行失败: {str(e)}")

            # 启动后台线程执行
            thread = threading.Thread(target=execute_playbook_job)
            thread.daemon = True
            thread.start()

            return Response({
                'status': 'success',
                'data': {
                    'job_id': job.id,
                    'message': 'Playbook任务已提交，开始执行'
                }
            })

        except Exception as e:
            logger.error(f"提交Playbook执行任务失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': f'提交任务失败：{str(e)}'
            }, status=500)


class AnsibleHostFileViewSet(viewsets.ViewSet):
    """Ansible主机文件管理ViewSet"""
    permission_classes = [IsAuthenticated]

    def get_host_directories(self, request, host_id):
        """获取主机目录结构"""
        try:
            path = request.query_params.get('path', '/')

            # 获取Ansible配置信息
            from .models import AnsibleConfig
            try:
                config = AnsibleConfig.objects.get(id=host_id)
            except AnsibleConfig.DoesNotExist:
                return Response({
                    'success': False,
                    'message': f'Ansible配置不存在: {host_id}'
                }, status=404)

            # 使用SSH连接到主机获取真实目录结构
            import paramiko
            import stat

            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

            try:
                # 连接到主机
                if config.auth_type == 'password':
                    ssh.connect(
                        hostname=config.host,
                        port=config.port,
                        username=config.username,
                        password=config.password,
                        timeout=10
                    )
                elif config.auth_type == 'key':
                    import tempfile
                    import os
                    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as key_file:
                        key_file.write(config.private_key)
                        key_path = key_file.name

                    try:
                        ssh.connect(
                            hostname=config.host,
                            port=config.port,
                            username=config.username,
                            key_filename=key_path,
                            timeout=10
                        )
                    finally:
                        os.unlink(key_path)

                # 使用SFTP获取目录内容
                sftp = ssh.open_sftp()

                directories = []
                files = []

                try:
                    # 列出目录内容
                    items = sftp.listdir_attr(path)

                    for item in items:
                        item_name = item.filename
                        item_path = f"{path.rstrip('/')}/{item_name}" if path != '/' else f"/{item_name}"

                        if stat.S_ISDIR(item.st_mode):
                            # 目录
                            directories.append({
                                'name': item_name,
                                'path': item_path,
                                'type': 'directory',
                                'size': item.st_size,
                                'modified': item.st_mtime
                            })
                        elif stat.S_ISREG(item.st_mode):
                            # 文件
                            files.append({
                                'name': item_name,
                                'path': item_path,
                                'type': 'file',
                                'size': item.st_size,
                                'modified': item.st_mtime
                            })

                    # 按名称排序
                    directories.sort(key=lambda x: x['name'])
                    files.sort(key=lambda x: x['name'])

                except FileNotFoundError:
                    return Response({
                        'success': False,
                        'message': f'目录不存在: {path}'
                    }, status=404)
                except PermissionError:
                    return Response({
                        'success': False,
                        'message': f'没有权限访问目录: {path}'
                    }, status=403)
                finally:
                    sftp.close()

            finally:
                ssh.close()

            # 检查是否有YAML文件
            yaml_files = [f for f in files if f['name'].lower().endswith(('.yml', '.yaml'))]

            return Response({
                'success': True,
                'directories': directories,
                'files': files,
                'yaml_files': yaml_files,
                'current_path': path,
                'has_yaml_files': len(yaml_files) > 0
            })

        except Exception as e:
            logger.error(f"获取主机目录失败: {str(e)}")
            return Response({
                'success': False,
                'message': f'获取目录失败：{str(e)}'
            }, status=500)

    def get_playbook_files(self, request, host_id):
        """获取指定目录下的Playbook文件"""
        try:
            path = request.query_params.get('path', '/opt/playbooks')

            # 模拟Playbook文件列表
            files = [
                {
                    'name': 'deploy-web.yml',
                    'path': f'{path}/deploy-web.yml',
                    'size': 1024,
                    'modified': '2024-01-15 10:30:00',
                    'type': 'file'
                },
                {
                    'name': 'update-system.yml',
                    'path': f'{path}/update-system.yml',
                    'size': 2048,
                    'modified': '2024-01-14 15:20:00',
                    'type': 'file'
                },
                {
                    'name': 'backup-data.yml',
                    'path': f'{path}/backup-data.yml',
                    'size': 1536,
                    'modified': '2024-01-13 09:15:00',
                    'type': 'file'
                }
            ]

            return Response({
                'status': 'success',
                'data': files
            })

        except Exception as e:
            logger.error(f"获取Playbook文件失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': str(e)
            }, status=500)

    def get_playbook_content(self, request, host_id):
        """获取Playbook文件内容"""
        try:
            file_path = request.query_params.get('file_path')
            if not file_path:
                return Response({
                    'success': False,
                    'message': '缺少file_path参数'
                }, status=400)

            # 获取Ansible配置信息
            from .models import AnsibleConfig
            try:
                config = AnsibleConfig.objects.get(id=host_id)
            except AnsibleConfig.DoesNotExist:
                return Response({
                    'success': False,
                    'message': f'Ansible配置不存在: {host_id}'
                }, status=404)

            # 使用SSH连接到主机读取文件内容
            import paramiko
            import tempfile
            import os

            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

            try:
                # 连接到主机
                if config.auth_type == 'password':
                    ssh.connect(
                        hostname=config.host,
                        port=config.port,
                        username=config.username,
                        password=config.password,
                        timeout=10
                    )
                elif config.auth_type == 'key':
                    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as key_file:
                        key_file.write(config.private_key)
                        key_path = key_file.name

                    try:
                        ssh.connect(
                            hostname=config.host,
                            port=config.port,
                            username=config.username,
                            key_filename=key_path,
                            timeout=10
                        )
                    finally:
                        os.unlink(key_path)

                # 读取文件内容
                stdin, stdout, stderr = ssh.exec_command(f'cat "{file_path}"')
                content = stdout.read().decode('utf-8')
                error = stderr.read().decode('utf-8')

                if error:
                    return Response({
                        'success': False,
                        'message': f'读取文件失败: {error}'
                    }, status=500)

            finally:
                ssh.close()

            return Response({
                'success': True,
                'content': content,
                'file_path': file_path
            })

        except Exception as e:
            logger.error(f"获取Playbook内容失败: {str(e)}")
            return Response({
                'success': False,
                'message': f'获取文件内容失败: {str(e)}'
            }, status=500)

    def save_playbook_content(self, request, host_id):
        """保存Playbook文件内容"""
        try:
            file_path = request.data.get('file_path')
            content = request.data.get('content')

            if not all([file_path, content]):
                return Response({
                    'status': 'error',
                    'message': '缺少必要参数：file_path, content'
                }, status=400)

            # 获取Ansible配置
            try:
                config = AnsibleConfig.objects.get(id=host_id)
            except AnsibleConfig.DoesNotExist:
                return Response({
                    'status': 'error',
                    'message': f'Ansible配置不存在: {host_id}'
                }, status=404)

            # 通过SSH连接保存文件
            import paramiko

            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

            try:
                # 连接到主机
                if config.auth_type == 'password':
                    ssh.connect(
                        hostname=config.host,
                        port=config.port,
                        username=config.username,
                        password=config.password,
                        timeout=10
                    )
                elif config.auth_type == 'key':
                    import tempfile
                    import os

                    # 创建临时私钥文件
                    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as key_file:
                        key_file.write(config.private_key)
                        key_path = key_file.name

                    try:
                        os.chmod(key_path, 0o600)
                        ssh.connect(
                            hostname=config.host,
                            port=config.port,
                            username=config.username,
                            key_filename=key_path,
                            timeout=10
                        )
                    finally:
                        os.unlink(key_path)

                # 使用SFTP保存文件
                sftp = ssh.open_sftp()

                # 确保目录存在
                dir_path = '/'.join(file_path.split('/')[:-1])
                if dir_path:
                    try:
                        sftp.mkdir(dir_path)
                    except:
                        pass  # 目录可能已存在

                # 写入文件内容
                with sftp.open(file_path, 'w') as remote_file:
                    remote_file.write(content)

                sftp.close()
                ssh.close()

                logger.info(f"成功保存Playbook文件: {file_path}")

                return Response({
                    'status': 'success',
                    'message': '文件保存成功'
                })

            except Exception as ssh_error:
                logger.error(f"SSH连接或文件保存失败: {str(ssh_error)}")
                return Response({
                    'status': 'error',
                    'message': f'保存文件失败: {str(ssh_error)}'
                }, status=500)
            finally:
                try:
                    ssh.close()
                except:
                    pass

        except Exception as e:
            logger.error(f"保存Playbook文件失败: {str(e)}")
            return Response({
                'status': 'error',
                'message': str(e)
            }, status=500)
